﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace RockBus.Threading
{
    public class MapReduceResult<TState, TInput, TResult> : EventArgs
    {
        public MapReduceResult(TState state, TInput input, TResult result)
        {
            this.State = state;
            this.Input = input;
            this.Result = result;
        }

        public MapReduceResult(TState state, TInput input, Exception exception)
        {
            this.State = state;
            this.Input = input;
            this.Exception = exception;
        }

        public TState State { get; private set; }

        public TInput Input { get; private set; }

        public TResult Result { get; set; }

        public Exception Exception { get; set; }
    }

    public class MapReduceResultList<TState, TInput, TResult> : List<MapReduceResult<TState, TInput, TResult>>
    {
        public MapReduceResultList()
            : base()
        {
        }

        public MapReduceResultList(IEnumerable<MapReduceResult<TState, TInput, TResult>> collection)
            : base(collection)
        {
        }

        public MapReduceResultList(int capacity)
            : base(capacity)
        {
        }

        public bool ContainsException()
        {
            return (this.Any((mrr) => mrr.Exception != null));
        }

        public IEnumerable<TResult> GetResults()
        {
            return this.Where((mrr) => mrr.Result != null).Select((mrr) => mrr.Result);
        }

        public IEnumerable<Exception> GetExceptions()
        {
            return this.Where((mrr) => mrr.Exception != null).Select((mrr) => mrr.Exception);
        }
    }

    public interface IEventingMapReduce
    {
    }

    public abstract class EventingMapReduceBase<TState, TInput, TResult>
    {
        protected EventingMapReduceBase(TState state, IEnumerable<TInput> inputs)
        {
            this.State = state;
            this.Inputs = new List<TInput>(inputs);
            this._mapTasks = new List<Task<MapReduceResult<TState, TInput, TResult>>>(this.Inputs.Count());
        }

        public TState State { get; private set; }

        protected List<TInput> Inputs { get; set; }

        public event EventHandler<ResultEventArgs<TState, TInput, TResult>> OnResult;

        public virtual Task<MapReduceResultList<TState, TInput, TResult>> ExecuteAsync()
        {
            this.Map();
            return this.Reduce();
        }

        //public virtual void AddTask(Task<TResult> mapFunc, params TInput[] inputs)
        //{
        //    this.AddTask(mapFunc, (IEnumerable<TInput>)inputs);
        //}

        private volatile bool _hasNewTasks = true;

        public virtual void AddTask(Task<TResult> mapFunc, TInput input)
        {
            this._hasNewTasks = true;
            var mapTask = this.CreateMapTask(mapFunc, input);
            this._mapTasks.Add(mapTask);
        }

        private readonly List<Task<MapReduceResult<TState, TInput, TResult>>> _mapTasks;

        private void Map()
        {
            foreach (var input in this.Inputs)
            {
                var mapTask = this.CreateMapTask(input);
                _mapTasks.Add(mapTask);
            }
        }

        private async Task<MapReduceResultList<TState, TInput, TResult>> Reduce()
        {
            // Wait in a while loop for any additional tasks to be completed
            //  These can be tasks that were added to EventingMapReduce by the mapTasks (via the events)
            while (_hasNewTasks)
            {
                this._hasNewTasks = false;
                await Task.Factory.ContinueWhenAll(this._mapTasks.ToArray(), tasks => { });
            }
            return new MapReduceResultList<TState, TInput, TResult>(this._mapTasks.Select((mt) => mt.Result));
        }

        protected void FireOnResult(MapReduceResult<TState, TInput, TResult> mrr)
        {
            var rea = new ResultEventArgs<TState, TInput, TResult>(this, mrr);
            this.OnResult.Fire(this, rea);
        }

        protected abstract Task<MapReduceResult<TState, TInput, TResult>> CreateMapTask(TInput input);

        protected Task<MapReduceResult<TState, TInput, TResult>> CreateMapTask(Task<TResult> mapTask, TInput input)
        {
            MapReduceResult<TState, TInput, TResult> mrr = null;
            var successTask = mapTask.ContinueWith((antecedent) =>
            {
                //Trace.WriteLine(string.Format("successTask; antecedent.Status: {0}", antecedent.Status));
                mrr = new MapReduceResult<TState, TInput, TResult>(this.State, input, antecedent.Result);
                this.FireOnResult(mrr);
            }, TaskContinuationOptions.OnlyOnRanToCompletion);
            //var successCompletionTask = successTask.ContinueWith((antecedent) =>
            //{
            //    Trace.WriteLine(string.Format("successCompletionTask; antecedent.Status: {0}", antecedent.Status));
            //});
            var failureTask = mapTask.ContinueWith((antecedent) =>
            {
                //Trace.WriteLine(string.Format("failureTask; antecedent.Status: {0}", antecedent.Status));
                mrr = new MapReduceResult<TState, TInput, TResult>(this.State, input, antecedent.Exception);
                this.FireOnResult(mrr);
            }, TaskContinuationOptions.OnlyOnFaulted);
            //var failureCompletionTask = failureTask.ContinueWith((antecedent) =>
            //{
            //    Trace.WriteLine(string.Format("failureCompletionTask; antecedent.Status: {0}", antecedent.Status));
            //});

            return Task.Factory.ContinueWhenAll(new[] { successTask, failureTask }, (t) => { return mrr; });
        }
    }

    // RoutingMessage, RoutingHeader, Message
    public class EventingFuncMapReduce<TState, TInput, TResult> : EventingMapReduceBase<TState, TInput, TResult>
    {
        public EventingFuncMapReduce(TState state, Func<TState, TInput, TResult> mapFunc, params TInput[] inputs)
            : this(state, mapFunc, (IEnumerable<TInput>)inputs)
        {
        }

        public EventingFuncMapReduce(TState state, Func<TState, TInput, TResult> mapFunc, IEnumerable<TInput> inputs)
            : base(state, inputs)
        {
            this.MapFunc = mapFunc;
        }

        private Func<TState, TInput, TResult> MapFunc { get; set; }

        protected override Task<MapReduceResult<TState, TInput, TResult>> CreateMapTask(TInput input)
        {
            var mapTask = Task.Factory.StartNew(() => { return this.MapFunc(this.State, input); });
            return base.CreateMapTask(mapTask, input);
        }
    }

    public class EventingTaskMapReduce<TState, TInput, TResult> : EventingMapReduceBase<TState, TInput, TResult>
    {
        public EventingTaskMapReduce(TState state, Func<TState, TInput, Task<TResult>> mapFunc, params TInput[] inputs)
            : this(state, mapFunc, (IEnumerable<TInput>)inputs)
        {
        }

        public EventingTaskMapReduce(TState state, Func<TState, TInput, Task<TResult>> mapFunc, IEnumerable<TInput> inputs)
            : base(state, inputs)
        {
            this.MapFunc = mapFunc;
        }

        private Func<TState, TInput, Task<TResult>> MapFunc { get; set; }

        protected override Task<MapReduceResult<TState, TInput, TResult>> CreateMapTask(TInput input)
        {
            var mapTask = this.MapFunc(this.State, input);
            return base.CreateMapTask(mapTask, input);
        }
    }

    public class ResultEventArgs<TState, TInput, TResult> : EventArgs
    {
        public ResultEventArgs(EventingMapReduceBase<TState, TInput, TResult> eventingMapReduce, MapReduceResult<TState, TInput, TResult> mapReduceResult)
        {
            this.EventingMapReduce = eventingMapReduce;
            this.MapReduceResult = mapReduceResult;
        }

        public EventingMapReduceBase<TState, TInput, TResult> EventingMapReduce { get; private set; }

        public MapReduceResult<TState, TInput, TResult> MapReduceResult { get; private set; }
    }
}